Skip to content

host: a team file linking to a file it cannot read is still reported as read through the link - #2432

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-linked-state-unreadable
Nov 4, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-linked-state-unreadable

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2428
Owner: Dev-2
Written-by: Dev-2
Reviewer: Birfy
Claim-Id: dd4eb92fee9e442d8996b3752516122f
Branch: dev/2-linked-state-unreadable
Tip: 350cc75
Tree: 318a91e0d93f4bc4f3c4a0d004f123d475e97a47
Base: 16d1645
Diff: 6 files, +89/−10 (hand-written 4 = +43/−4; generated 2 = +46/−6)
Tier: T2 (why: [])

What changed

stateNotPlain() (core/src/host/diagnostics.js) followed a symbolic link with stat() and asked only isFile(). A file whose read bit is clear still answers isFile() === true, so a state.json linking to it earned the link sentence — not a plain file (described only; the store reads the team file through the link and the next save replaces the name) — and the very next row of the same report said that read failed: | Read-only | The Bot team file … could not be read (EACCES). … |. Both halves cannot be true: readFile(statePath) throws before writable() can become true, so no save ever renames over the name.

The target's read permission is now asked as well, with access(path, R_OK) — it opens nothing and reads no byte, which is all this report may do with the team file. A link to a file this host may not read keeps the leaf sentence not a plain file (nothing is read or written), the same one a link to a folder already gets for EISDIR, and the state line now agrees with the Read-only row below it. A link to a readable plain file is unchanged, and so is a link to a folder or to nothing.

patches/ds-bot/0001-core-fixes.patch and 0007-version-and-tooling.patch are what node scripts/ds-bot-patches.mjs generate writes; nothing in them is hand-edited.

Red first

On a clean 16d16456 worktree with only core/test/diagnostics.test.js from this branch, the file was 58 tests / 57 pass / 1 fail / 0 skipped / 0 cancelled (log tmp/p555-red-final.log):

not ok 29 - a team file linking to a file it cannot read keeps the leaf sentence, not the read-through-link one
  expected: 'not a plain file (nothing is read or written)'
  actual: 'not a plain file (described only; the store reads the team file through the link and the next save replaces the name)'

The case builds a real mode 000 target behind a symlinked state.json, and also asserts the Read-only row naming EACCES, so it pins both halves of the contradiction. It declares skip: as a test option when the run is root (root reads a file without its read bits), the way the neighbouring #2364 case skips that shape; with the fix the file is 58/58 on the same base.

Ran

TZ=UTC ARCH_DOCS_BASE=16d16456 npm run verify
node .github/scripts/changelog.mjs check
node .github/scripts/check-empty-catch.mjs --check
node .github/scripts/check-mojibake.mjs --check
node scripts/ds-bot-patches.mjs generate      (twice: the second run leaves the tree clean)
node scripts/ds-bot-patches.mjs canonical

4844 = 2852+574+132+141+411+671+37+26, 0 fail / 0 cancelled; lint 0 errors (core 77 warnings, plugins 36/9/2/16); arch-docs: OK (6 changed file(s)); 10 patches apply cleanly, core/ equals ds-bot + patches, and generate is a no-op on the second run; build-client --check exit 0; canonical ok.

Not run

The smoke test (this touches no route and no client), CI's package job (pack check, typecheck, actionlint), and CI itself. The new case cannot run as root by design, so a root-only CI runner would mark it skipped.

Docs: docs/subsystems/core-host.md — the report's state-file bullet now says a link to a plain file this host may not read keeps the leaf sentence, and names access(path, R_OK) as the check.

{
"pr": 2432,
"claim": "Dev-2",
"issue": 2428,
"summary": "diagnostics.js stateNotPlain() followed a link with stat() and asked only isFile(), so a state.json linking to a file this host cannot read still earned the read-through-the-link sentence, directly above a Read-only row reporting that same read failing with EACCES. It now also asks access(path, R_OK), which opens nothing and reads no byte, so such a link keeps the leaf sentence a link to a folder already gets for EISDIR. One test builds a mode-000 target behind the symlink and pins both halves, skipping under root.",
"files": [
"changelog.d/2428-linked-state-unreadable.fixed.md",
"core/src/host/diagnostics.js",
"core/test/diagnostics.test.js",
"docs/subsystems/core-host.md",
"patches/ds-bot/0001-core-fixes.patch",
"patches/ds-bot/0007-version-and-tooling.patch"
],
"checks": {
"ran": "TZ=UTC ARCH_DOCS_BASE=16d16456 npm run verify",
"result": "green",
"at": "350cc75cb24381582595a33ed0ea8d9be50223cf"
},
"next": "ready for review; the first review's tip is 350cc75"
}

@HugoXK

HugoXK commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request for this exact tip: 8f53e37de0 (base 16d16456, non-draft, maintainer_can_modify: true).

Red-first on a clean base plus the four gates are in the PR body; enqueue-guard is red only because no Verifier report names this tip yet.

Please review 8f53e37de0 and post Verdict: PASS (or the changes you want). No new tip will be pushed for CI noise.

… read through the link

stateNotPlain() followed a symbolic link with stat() and asked only isFile(), so a
target this host may not read still earned the link sentence: 'the store reads the
team file through the link and the next save replaces the name'. The next row of the
same report then said that read failed with EACCES, and no save ever renames over the
name, because readFile() throws before writable() can become true.

The target's read permission is now asked with access(path, R_OK) — no open, no byte —
so a link to an unreadable file keeps the leaf sentence, which matches the Read-only
row below it. Covered by one case that skips when the run is root.

patches/ds-bot/0001-core-fixes.patch and 0007-version-and-tooling.patch are what
generate writes.
@HugoXK
HugoXK force-pushed the dev/2-linked-state-unreadable branch from 8f53e37 to 350cc75 Compare November 4, 2026 18:15
@HugoXK

HugoXK commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request for this exact tip: 350cc75cb24381582595a33ed0ea8d9be50223cf (base 16d16456f14023a24228730a98487b585cf08441, Tier: T2, non-draft, maintainer_can_modify: true).

The tip moved once since the earlier request, and only to fix a placeholder the earlier head carried: core/test/diagnostics.test.js:796 said // #R: (now // #2428:) plus two stray blank lines. No code, no test body changed; the earlier head had no Verdict, so nothing was lost.

Ready at this tip: red-first on a clean 16d16456 with only the new case is 57 pass / 1 fail (the state file linking to a mode-000 file is still described as read through the link), the tip is 58/58 with 0 skipped (the EACCES case runs here, not skipped); npm run verify is 4844 with 0 fail, patches check 10 patches + core/ equals ds-bot/, generate second run no diff, arch-docs OK(6), lint 0 errors. checkpointReasons() on the current body is [].

FlyBot Test read this change earlier at 8f53e37de0 and passed it (58/58, strace shows zero openat/read on the target, and the base's contradictory pair is reproducible there); the only thing it asked for was the placeholder above, which this tip fixes.

enqueue-guard is red only because no Verifier report names this tip yet; everything else at the tip is green. Please review 350cc75c and post Verdict: PASS (or the changes you want). No new tip will be pushed for CI noise.

@Birfy

Birfy commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 350cc75
Written-by: Reviewer
Attestation: 350cc75 role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/diagnostics.test.js
# tests 58  # pass 58  # fail 0  # skipped 0  # cancelled 0
$ cp /data/dsh/bot/workspace/p2432base/core/src/host/diagnostics.js core/src/host/diagnostics.js
$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/diagnostics.test.js
# tests 58  # pass 57  # fail 1  # skipped 0  # cancelled 0

Read at this tip (tip tarball extracted to /data/dsh/bot/workspace/p2432tip, base 16d16456 to p2432base):

  • Red first, one revert. With the tip's own test file and only core/src/host/diagnostics.js swapped back to its base blob, the new case fails exactly as described: not ok 29 - a team file linking to a file it cannot read keeps the leaf sentence, not the read-through-link one, expected not a plain file (nothing is read or written), actual not a plain file (described only; the store reads the team file through the link and the next save replaces the name). Restored, the file is 58/58 with 0 skipped — the 000 case really ran here rather than skipping.
  • The fix is minimal and asks no byte. stateNotPlain() now asks stat(path).isFile() and then access(path, constants.R_OK) before returning LINKED_STATE; a target this host may not read falls to NOT_PLAIN, the same leaf sentence a link to a folder already gets for EISDIR. No open, no readFile, no createReadStream on state.json — the report keeps its "describe, never read" property, which is the one rule this file exists to keep. access follows the link, so the permission asked is the target's, which is the one readFile(statePath) fails on.
  • Both halves of the contradiction are pinned. The case builds a real mode 000 target behind a symlinked state.json and asserts the state line and the Read-only row naming EACCES, so a later change cannot satisfy one and reintroduce the other. It restores the mode in t.after and in the body, so no 000 file survives a failed assertion, and it skips under root where the kernel does not enforce the bits — the reason is in the case, not in a comment elsewhere.
  • CI at this tip: tests-ok, Node 24 · DSH 0.2.0-rc.2, package, merge-gate, the three plugin jobs and reviewed tip (report only) all success; enqueue-guard is red only with do not enqueue #2432: no Verifier report names the tip 350cc75cb243, which this report clears. No hold, no stop comment after the tip.
  • Scope and trail: 6 files, +89/−10 — the source fix, one test case, one core-host.md bullet naming access(path, R_OK), one changelog fragment, and the two generated patches. docs/subsystems/core-host.md is an owned path and moved in the same diff, so arch-docs is satisfied by content and not by a trailer. The changelog fragment is a .fixed.md for host: a team file linking to a file it cannot read is still reported as read through the link #2428, which the body closes.

Not run by me: the full suite, npm run verify, the smoke test, and the package job — its green at this tip stands in, and the change touches no route and no client. Reading only, no file of this change set was modified.

One note, not a blocker: the body's Reviewer: line names Birfy, which #1228 refuses on a branch this repository owns because it names no workstation. This is a fork (HugoXK/FlyBot), and the guard exempts a fork from the Owner:/Reviewer: and agent-pr checks, so nothing is held up here; it would need a role name if this change were ever moved onto an owned branch.

@github-actions github-actions Bot added the pass label Nov 4, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 4, 2026
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 4, 2026
Merged via the queue into ThinkFlowLab:main with commit 648944d Nov 4, 2026
16 checks passed
@Birfy
Birfy deleted the dev/2-linked-state-unreadable branch November 4, 2026 18:28
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: a team file linking to a file it cannot read is still reported as read through the link

2 participants